package yawning.mew.cat;

import java.util.EnumSet;

import yawning.mew.BuffsDebuffs.Buff;
import yawning.mew.BuffsDebuffs.Debuff;
import yawning.mew.ProcEffect;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.Procs;
import yawning.mew.cat.CatModelSimulation.Action;
import yawning.mew.character.CharacterModel.AttackResult;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.Strategy.IStatus;

public class CatModelSimState implements IStatus {
	// Begin Public Methods available to Strategy
	public final double getTimeRemaining() {
		return mSimUtil.getEndTime() - mSimUtil.getSimTime();
	}

	public final boolean isInBloodInTheWaterRange() {
		return bitwEv.mInRange;
	}

	public final boolean isInCombat() {
		return mSimUtil.getSimTime() > 0.0;
	}

	public final double getMissChance() {
		return mModel.getMissChance();
	}

	public final double getDodgeChance() {
		return mModel.getDodgeChance();
	}

	public final double getCritChance() {
		return mModel.getMeleeCritChance();
	}

	public final int getEnergy() {
		return energyTickEv.mEnergy;
	}

	public final int getEnergyCap() {
		return energyTickEv.mEnergyCap;
	}

	public final double getEnergyTickInterval() {
		return mModel.getEnergyTickInterval();
	}

	public final int getComboPoints() {
		return mComboPoints;
	}

	public final boolean isFeralFFUp() {
		return feralFFEv.mStacks > 0;
	}

	public final int getFeralFFStacks() {
		return feralFFEv.mStacks;
	}

	public final double getFeralFFRemaining() {
		return feralFFEv.mStacks > 0 ? feralFFEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final double getFeralFFCD() {
		return feralFFEv.mCooldownExpireTime > mSimUtil.getSimTime() ? feralFFEv.mCooldownExpireTime - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean isOoCUp() {
		return oocEv.isUp();
	}

	public final boolean isMangleUp() {
		return mangleEv.isUp();
	}

	public final double getMangleRemaining() {
		return mangleEv.isUp() ? mangleEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final int getMangleCost() {
		return getEnergyCost(mModel.mangleCost);
	}

	public final int getShredCost() {
		return getEnergyCost(mModel.shredCost);
	}

	public final int getRakeCost() {
		return getEnergyCost(mModel.rakeCost);
	}

	public final int getRipCost() {
		return getEnergyCost(mModel.ripCost);
	}

	public final boolean isRakeUp() {
		return rakeEv.isUp();
	}

	public final double getRakeRemaining() {
		return rakeEv.isUp() ? rakeEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final double getRakeBaseDuration() {
		return mModel.getRakeDebuffDuration();
	}

	public final boolean isRakeTFed() {
			return rakeEv.isUp() && rakeEv.mTigersFury;
	}

	public final boolean isRipUp() {
		return ripEv.isUp();
	}

	public final double getRipRemaining() {
		return ripEv.isUp() ? ripEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final double getRipBaseDuration() {
		return mModel.getRipDebuffDuration();
	}

	public final int getRipShredExtRemaining() {
		if (!ripEv.isUp()) return 0;
		return mModel.shredRipExtDuration > 0 ? 3 - ripEv.mShreds : 0;
	}

	public final boolean isRipTFed() {
		return ripEv.isUp() && ripEv.mTigersFury;
	}

	public final boolean isSavageRoarUp() {
		return savageRoarEv.isUp();
	}

	public final double getSavageRoarRemaining() {
		return savageRoarEv.isUp() ? savageRoarEv.getExpiryTime() - mSimUtil.getSimTime() :  0.0;
	}

	public final int getSavageRoarCost() {
		return mModel.srCost;
	}

	public final int getFerociousBiteCost() {
		return getEnergyCost(mModel.biteCost);
	}

	public final boolean isBerserkUp() {
		return berserkEv.isUp();
	}

	public final double getBerserkCD() {
		return berserkEv.mCooldownAt > mSimUtil.getSimTime() ? berserkEv.mCooldownAt - mSimUtil.getSimTime() : 0.0;
	}

	public final double getBerserkRemaining() {
		return berserkEv.isUp() ? berserkEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean isTigersFuryUp() {
		return tigersFuryEv.isUp();
	}

	public final double getTigersFuryCD() {
		return tigersFuryEv.mCooldownAt > mSimUtil.getSimTime() ? tigersFuryEv.mCooldownAt - mSimUtil.getSimTime() : 0.0;
	}

	public final double getTigersFuryRemaining() {
		return tigersFuryEv.isUp() ? tigersFuryEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final double getFeralChargeCD() {
		if (isInCombat()) return Double.MAX_VALUE;	// XXX: We only support this on opener.
		return feralFFEv.mCooldownExpireTime > mSimUtil.getSimTime() ? feralFFEv.mCooldownExpireTime - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean isStampedeUp() {
		return stampedeEv.isUp();
	}

	public final double getStampedeRemaining() {
		return stampedeEv.isUp() ? stampedeEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final double getSkullBashCD() {
		return mSkullBashAction.mCooldownAt > mSimUtil.getSimTime() ? mSkullBashAction.mCooldownAt - mSimUtil.getSimTime() : 0.0;
	}

	public final int getSkullBashCost() {
		return mModel.skullBashCost;	// XXX: Is this affected by Berserk/OoC?
	}

	public final boolean isHeroismUp() {
		return heroismEv.isUp();
	}

	public final double getHeroismCD() {
		return heroismEv.mCooldownExpireTime > mSimUtil.getSimTime() ? heroismEv.mCooldownExpireTime - mSimUtil.getSimTime() : 0.0;
	}

	public final double getHeroismRemaining() {
		return heroismEv.isUp() ? heroismEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean isUnholyFrenzyUp() {
		return unholyFrenzyEv.isUp();
	}

	public final double getUnholyFrenzyCD() {
		return unholyFrenzyEv.mCooldownAt > mSimUtil.getSimTime() ? unholyFrenzyEv.mCooldownAt - mSimUtil.getSimTime() : 0.0;
	}

	public final double getUnholyFrenzyRemaining() {
		return unholyFrenzyEv.isUp() ? unholyFrenzyEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean isShatteringThrowUp() {
		return shatteringThrowEv.isUp();
	}

	public final double getShatteringThrowCD() {
		return shatteringThrowEv.mCooldownExpireTime > mSimUtil.getSimTime() ? shatteringThrowEv.mCooldownExpireTime - mSimUtil.getSimTime() : 0.0;
	}

	public final double getShatteringThrowRemaining() {
		return shatteringThrowEv.isUp() ? shatteringThrowEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean hasPotion() {
		return mHavePotion;
	}

	public final boolean canPotion() {
		return (mHavePotion && !mPotionAction.mUsed);
	}

	public final boolean hasTier_11_4pc() {
		return mModel.druid.tier_11_4pc;
	}

	public final boolean isTier_11_4pcUp() {
		return tier_11_4pcEv.isUp();
	}

	public final int getTier_11_4pcStacks() {
		return (int) mModel.tier_11_4pc_buffStacks;
	}

	public final double getTier_11_4pcRemaining() {
		return mModel.tier_11_4pc_buffStacks > 0 ? tier_11_4pcEv.getExpiryTime() - mSimUtil.getSimTime() : 0.0;
	}

	public final boolean shouldFF() {
		return mShouldFF;
	}

	public final boolean shouldMangle() {
		return mShouldMangle;
	}
	// End Public Methods

	SimulationEngine mSimUtil;
	CatModel mModel;
	boolean mDebug = false;

	int mComboPoints = 0;

	EventBloodInTheWaterRange bitwEv;
	EventMeleeSwing meleeSwingEv;
	EventEnergyTick energyTickEv;
	EventRip ripEv;
	EventRake rakeEv;
	EventFeralFF feralFFEv;
	EventMangleDebuff mangleEv;
	EventSavageRoar savageRoarEv;
	EventOoC oocEv;
	EventBerserk berserkEv;
	EventTigersFury tigersFuryEv;
	EventStampede stampedeEv;
	EventToTT tottEv;
	EventHeroism heroismEv;
	EventUnholyFrenzy unholyFrenzyEv;
	EventShatteringThrow shatteringThrowEv;
	EventTier_11_4pc tier_11_4pcEv;

	ShredAction mShredAction;
	RavageAction mRavageAction;
	FerociousBiteAction mFerociousBiteAction;
	FeralChargeAction mFeralChargeAction;
	SkullBashAction mSkullBashAction;
	PotionAction mPotionAction;

	boolean mShouldFF;
	boolean mShouldMangle;
	boolean mHavePotion;

	CatModelSimState(SimulationEngine su) {
		mSimUtil = su;
		mModel = new CatModel(su.getToon(), su.getConfig());
		mModel.setSimulationEngine(mSimUtil);

		// Apply other buffs/debuffs.
		//
		// XXX: Most simulator scripts will cast Feral FF even if buffDebuff.isDebuff(Debuff.Armor)
		// is false.
		feralFFEv = new EventFeralFF(this);
		mangleEv = new EventMangleDebuff(this);

		if (mModel.buffsDebuffs.isBuff(Buff.TRICKSOFTHETRADE)) tottEv = new EventToTT(this);

		heroismEv = new EventHeroism(this);
		if (!mModel.buffsDebuffs.isBuff(Buff.MAJORHASTE)) heroismEv.mCooldownExpireTime = Double.MAX_VALUE;

		unholyFrenzyEv = new EventUnholyFrenzy(this);
		if (!mModel.buffsDebuffs.isBuff(Buff.UNHOLYFRENZY)) unholyFrenzyEv.mCooldownAt = Double.MAX_VALUE;

		shatteringThrowEv = new EventShatteringThrow(this);
		if (!mModel.buffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) shatteringThrowEv.mCooldownExpireTime = Double.MAX_VALUE;

		mShouldFF = mModel.buffsDebuffs.isDebuff(Debuff.ARMOR);
		mShouldMangle = mModel.buffsDebuffs.isDebuff(Debuff.BLEEDDMG);

		// Mob hits Feast Of Flesh range
		bitwEv = new EventBloodInTheWaterRange(this);
		meleeSwingEv = new EventMeleeSwing(this);
		energyTickEv = new EventEnergyTick(this, mModel.baseEnergyCap, mModel.baseEnergyCap);

		//
		// Preallocate the remaining events, and initialize the rest of our state.
		//
		savageRoarEv = new EventSavageRoar(this);
		ripEv = new EventRip(this);
		rakeEv = new EventRake(this);
		oocEv = new EventOoC(this);
		stampedeEv = new EventStampede(this);
		berserkEv = new EventBerserk(this);
		tigersFuryEv = new EventTigersFury(this);
		tier_11_4pcEv = new EventTier_11_4pc(this);

		mSimUtil.registerProcEffectCallback(Effect.PHYSICAL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(int n) {
				AttackResult result = mModel.getYellowResult();
				double damage = n;

				if (result != AttackResult.MISS && result != AttackResult.DODGE) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multCrit;
					}

					damage *= getDirectDamageMult();
					procPhysDamage += Math.round(damage);
				}
			}
		});
		mSimUtil.registerProcEffectCallback(Effect.SPELL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(int n) {
				AttackResult result = mModel.getSpellResult();
				double damage = n;

				if (result != AttackResult.MISS) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multSpellCrit;
					}

					// XXX: Factor in Spell Vulnerability here.
					procSpellDamage += Math.round(damage);
				}
			}
		});
		mSimUtil.registerProcEffectCallback(Effect.SPELL_ID, new EffectCallback() {
			int mDeathbringersWillStat;

			@Override
			public void onEffect(int n) {
				if (n == 50363) {
					mDeathbringersWillStat = mSimUtil.getRandom().nextInt(3);
					switch (mDeathbringersWillStat) {
					case 0: mModel.bonusStrength += 700; break;
					case 1: mModel.bonusAgility += 700; break;
					case 2: mModel.bonusHasteRating += 700; break;
					}
				} else if (n == 50362) {
					mDeathbringersWillStat = mSimUtil.getRandom().nextInt(3);
					switch (mDeathbringersWillStat) {
					case 0: mModel.bonusStrength += 600; break;
					case 1: mModel.bonusAgility += 600; break;
					case 2: mModel.bonusHasteRating += 600; break;
					}
				} else if (n == -50363) {
					switch (mDeathbringersWillStat) {
					case 0: mModel.bonusStrength -= 700; break;
					case 1: mModel.bonusAgility -= 700; break;
					case 2: mModel.bonusHasteRating -= 700; break;
					}
				} else if (n == -50362) {
					switch (mDeathbringersWillStat) {
					case 0: mModel.bonusStrength -= 600; break;
					case 1: mModel.bonusAgility -= 600; break;
					case 2: mModel.bonusHasteRating -= 600; break;
					}
				}
			}
		});

		mSimUtil.registerProcEffectCallback(Effect.SPELL_ID, new EffectCallback() {
			@Override
			public void onEffect(int n) {
				if (n == 26297) {
					mModel.hasteMult *= 1.2;
				} else if (n == -26297) {
					mModel.hasteMult /= 1.2;
				}
			}
		});

		for (ProcEffect effect : Procs.getInstance().getProcsByProperty(mSimUtil.getConfig().getProperties())) {
			if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
				mHavePotion = true;
			} else if (!effect.triggers.contains(ProcEffect.Trigger.USER) && effect.rate == 0 && effect.ppm == 0) {
				mSimUtil.getView().onWarn("Proc effect \"" + effect.name + "\" has no rate information.");
				continue;
			}

			mSimUtil.addProc(new SimulationProc(mSimUtil, effect));
		}

		mShredAction = new ShredAction(this);
		mRavageAction = new RavageAction(this);
		mFerociousBiteAction = new FerociousBiteAction(this);
		mFeralChargeAction = new FeralChargeAction(this);
		mSkullBashAction = new SkullBashAction(this);
		mPotionAction = new PotionAction(this);

		mSimUtil.addAction(Action.FERALCHARGE, mFeralChargeAction);
		mSimUtil.addAction(Action.TIGERSFURY, tigersFuryEv);
		mSimUtil.addAction(Action.SKULLBASH, mSkullBashAction);
		mSimUtil.addAction(Action.POTION, mPotionAction);
		mSimUtil.addAction(Action.HEROISM, heroismEv);
		mSimUtil.addAction(Action.UNHOLYFRENZY, unholyFrenzyEv);
		mSimUtil.addAction(Action.SHATTERINGTHROW, shatteringThrowEv);
		mSimUtil.addAction(Action.RAVAGE, mRavageAction);
		mSimUtil.addAction(Action.MANGLE, mangleEv);
		mSimUtil.addAction(Action.RAKE, rakeEv);
		mSimUtil.addAction(Action.SHRED, mShredAction);
		mSimUtil.addAction(Action.RIP, ripEv);
		mSimUtil.addAction(Action.BITE, mFerociousBiteAction);
		mSimUtil.addAction(Action.ROAR, savageRoarEv);
		mSimUtil.addAction(Action.FERALFF, feralFFEv);
		mSimUtil.addAction(Action.BERSERK, berserkEv);
	}

	//
	// Damage sources/various triggers.
	//

	final double getDirectDamageMult() {
		double mult = 1;

		mult *= (1 - mModel.multTargetArmor);
		mult *= (tigersFuryEv.isUp()) ? mModel.multTigersFury : 1.0;
		mult *= mModel.multPhysVuln;
		mult *= mModel.multDamageMult;
		mult *= mModel.multToTT;

		return mult;
	}

	final double getBleedDamageMult() {
		double mult = 1;

		mult *= mModel.multToTT;
		mult *= mModel.multBleedDarkIntent;
		mult *= mModel.getBleedMastery();
		mult *= mangleEv.isUp() ? mModel.multMangle : 1;
		mult *= mModel.multPhysVuln * mModel.multDamageMult;

		return mult;
	}

	final void onOoCConsumer(int cost) {
		if (oocEv.isUp()) {
			oocEv.cancel();
			oocEnergyReturn += cost;
		} else {
			energyTickEv.mEnergy -= cost;
		}
	}

	//
	// Ability use
	//
	final void onCpGenerator(boolean crit) {
		if (crit) {
			if (mModel.pPrimalFury == 1.0 || mSimUtil.getRandom().nextBoolean(mModel.pPrimalFury)) {
				mComboPoints = Math.min(5, mComboPoints + 2);
			} else {
				mComboPoints = Math.min(5, mComboPoints + 1);
			}
		} else {
			mComboPoints = Math.min(5, mComboPoints + 1);
		}
	}

	final int getEnergyCost(int baseCost) {
		return berserkEv.isUp() ? baseCost / 2 : baseCost;
	}

	final void onPrimalMadnessGain() {
		if (mModel.primalMadnessEnergyGain > 0) {
			if (energyTickEv.mEnergyCap == mModel.baseEnergyCap) {
				energyTickEv.mEnergyCap += mModel.primalMadnessEnergyGain;
				energyTickEv.mEnergy = Math.min(energyTickEv.mEnergyCap, energyTickEv.mEnergy + mModel.primalMadnessEnergyGain);
			}
		}
	}

	final void onPrimalMadnessLoss() {
		if (mModel.primalMadnessEnergyGain > 0) {
			if (energyTickEv.mEnergyCap > mModel.baseEnergyCap && !berserkEv.isUp() && !tigersFuryEv.isUp()) {
				energyTickEv.mEnergyCap = mModel.baseEnergyCap;

				energyTickEv.mEnergy = Math.min(mModel.baseEnergyCap, energyTickEv.mEnergy);
				energyTickEv.mEnergy = Math.max(0, energyTickEv.mEnergy - mModel.primalMadnessEnergyGain);
			}
		}
	}

	final void DBGF(String format, Object ... args) {
		if (mDebug) {
			System.out.println(mSimUtil.getSimTime() + ": " + String.format(format,args));
		}
	}

	long whiteDamage = 0;
	int nrWhiteMiss = 0;
	int nrWhiteDodge = 0;
	int nrWhiteGlance = 0;
	int nrWhiteCrit = 0;
	int nrWhiteHit = 0;

	long furySwipesDamage = 0;
	int nrFurySwipesMiss = 0;
	int nrFurySwipesDodge = 0;
	int nrFurySwipesCrit = 0;
	int nrFurySwipesHit = 0;

	long mangleDamage = 0;
	int nrMangleMiss = 0;
	int nrMangleDodge = 0;
	int nrMangleCrit = 0;
	int nrMangleHit = 0;

	long shredDamage = 0;
	int nrShredMiss = 0;
	int nrShredDodge = 0;
	int nrShredCrit = 0;
	int nrShredHit = 0;

	long rakeDamage = 0;
	int nrRakeMiss = 0;
	int nrRakeDodge = 0;
	int nrRakeCrit = 0;
	int nrRakeHit = 0;

	long biteDamage = 0;
	int nrBiteMiss = 0;
	int nrBiteDodge = 0;
	int nrBiteCrit = 0;
	int nrBiteHit = 0;

	long rakeDotDamage = 0;
	int nrRakeDotCrit = 0;
	int nrRakeDotHit = 0;

	long ripDamage = 0;
	int nrRipMiss = 0;
	int nrRipDodge = 0;
	int nrRipHit = 0;
	int nrRipDotCrit = 0;
	int nrRipDotHit = 0;

	long ravageDamage = 0;
	int nrRavageMiss = 0;
	int nrRavageHit = 0;
	int nrRavageDodge = 0;
	int nrRavageCrit = 0;

	int nrSR = 0;
	int nrSrCPs = 0;

	int nrTF = 0;
	int nrBerserk = 0;

	int nrFFMiss = 0;
	int nrFFHit = 0;

	int nrFeralCharge = 0;

	int nrOoCProcs = 0;
	long oocEnergyReturn = 0;

	long procPhysDamage = 0;
	long procSpellDamage = 0;

	// Feast of Flesh related stats.
	int nrBiteBase = 0;
	int nrRipBase = 0;
	int nrRipFailBase = 0;
	long ripDamageBase = 0;

	long totalDamageBase = 0;
}
